<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns=http://www.w3.org/1999/xhtml style><!--
 Page saved with SingleFile 
 url: https://linux.cn/article-9703-1.html 
 saved date: Wed Mar 11 2020 18:59:38 GMT+0800 (China Standard Time)
--><meta charset=utf-8>
<title>技术|Systemd 服务：比启动停止服务更进一步</title>
<meta name=keywords content="Systemd 服务：比启动停止服务更进一步">
<meta name=description content="让我们更进一步，让其可以向玩家发邮件，包括在服务器可用时通知玩家，在服务器关闭前警告玩家 ">
<meta name=copyright content="2005-2019 Linux.cn">
<meta name=viewport content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta name=applicable-device content=pc,mobile>
<meta name=referrer content=always>
<style>:root{--sf-img-3:url("");--sf-img-5:url("");--sf-img-6:url("")}*{word-wrap:break-word}body{background:#dce2e7 url() repeat-x 0 0;position:relative}body,input,button{font:12px/1.8 'Lantinghei SC','Helvetica Neue','Microsoft YaHei',微软雅黑,Arial,STHeiti,'WenQuanYi Micro Hei',SimSun,sans-serif;color:#444}body,ul,li,p,h3,form{margin:0;padding:0}table{empty-cells:show;border-collapse:collapse}ul li,.xl li{list-style:none}h1,h3{font-size:1em}h1,h3{font-family:'Microsoft YaHei','Hiragino Sans GB','STHeiti',Tahoma,'SimHei',sans-serif}em{font-style:normal}a{color:#036;text-decoration:none}a:hover{text-decoration:underline}a img{border:0}::selection{background:#2267b5;color:#FFF}::-webkit-selection{background:#2267b5;color:#FFF}a::selection{color:#CCC}a::-webkit-selection{color:#CCC}.z{float:left}.y{float:right;padding-left:5px}.textcut{max-width:60%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.cl:after{content:".";display:block;height:0;clear:both;visibility:hidden}.cl{zoom:1}.hidefocus{outline:0}hr{display:block;clear:both}.mn hr{margin:0 10px}.ph{font-family:'Microsoft YaHei','Hiragino Sans GB','STHeiti',Tahoma,'SimHei',sans-serif;font-weight:100}.pipe{margin:0 5px;color:#CCC}.xg1{color:#999!important}.xi2{color:#369}.bbs{border-bottom:1px solid #cdcdcd!important}.pbn{padding-bottom:5px!important}.ptm{padding-top:10px!important}.pbm{padding-bottom:10px!important}.hm{text-align:center}.pn{vertical-align:middle;overflow:hidden;margin-right:3px;height:23px;cursor:pointer;-moz-box-shadow:0 1px 0 #e5e5e5;-webkit-box-shadow:0 1px 0 #e5e5e5}.pn:active{background-position:0 -23px}.pn strong{padding:0 10px}.pn strong{font-weight:700}.pnc{border-color:#235994;background-color:#06C;background-position:0 -48px;color:#FFF!important}.pnc:active{background-position:0 -71px}.wp{margin:0 auto}#wp .wp{width:auto}#hd{margin-bottom:5px;border-bottom:0 solid #c2d5e3}#hd .wp{padding:0}#hd h1{display:block;margin:24px;padding:0;position:absolute;width:128px;height:128px}#topmenu{background:#333 var(--sf-img-3) no-repeat 0 0}#scbar{float:right;overflow:hidden;height:45px;line-height:42px;border-top:1px solid #FFF;border-bottom:1px solid #EEE;background-image:linear-gradient(to bottom,#FFF,#EEE);background-color:#94f592;width:1020px;padding-left:180px}.scbar_icon_td{width:21px;background:var(--sf-img-5) no-repeat 0 -50px}.scbar_txt_td{background:var(--sf-img-5) repeat-x 0 -93px}#scbar_txt{width:100px;border:1px solid #FFF;outline:0}.scbar_btn_td{width:38px;background:var(--sf-img-5) no-repeat 8px -142px}#scbar_btn{margin:0;padding:0;border:0;background:transparent none;box-shadow:none}#scbar_btn strong{line-height:84px}.scbar_type_td{width:61px;background:var(--sf-img-5) no-repeat 0 -193px}#scbar_type{display:block;padding-left:10px;text-align:left;text-decoration:none}#scbar_type_menu{margin-top:-8px}#scbar_hot{padding-left:8px;height:45px;overflow:hidden}#nv{overflow:hidden;height:33px;background:#333 var(--sf-img-3) no-repeat 0 0}#nv li{float:left;padding-right:1px;height:33px;line-height:33px;background:var(--sf-img-6) no-repeat 100% 0;font-size:14px}#nv li a{float:left;padding:0 15px;height:33px;font-family:'Microsoft YaHei','Hiragino Sans GB','STHeiti',Tahoma,'SimHei',sans-serif;font-size:16px}#nv li a{color:#FFF}#nv li a:hover{background:var(--sf-img-6) no-repeat 50% -66px}#mu{position:relative;z-index:1}#mu ul{border-top:1px solid #FFF;border-bottom:1px solid #EEE;background-image:linear-gradient(to bottom,#FFF,#EEE);background-color:#FFF;line-height:22px;z-index:2;font-size:12px;height:45px;position:absolute;width:100%}#mu li{float:left;height:32px;line-height:35px}.mutag a{font-family:'Microsoft YaHei','Hiragino Sans GB','STHeiti',Tahoma,'SimHei',sans-serif;font-size:14px}.floatmu{left:0;top:0}.titletext{font-family:'Microsoft YaHei','Hiragino Sans GB','STHeiti',Tahoma,'SimHei',sans-serif;font-size:14px}#mn_userapp_menu{clear:left;min-height:210px;min-width:630px}#ct{min-height:300px}.mn{overflow:hidden}.ct2 .mn{float:left;margin-bottom:1em}#ft{padding:10px 0 50px;background:none repeat scroll 0 0 #555;line-height:1.8;color:#666}#scrolltop{position:fixed;bottom:100px;display:block;margin:-30px 20px 0 2px;width:40px;background:#f4f4f4;border:1px #cdcdcd solid;border-radius:3px;border-top:0;cursor:pointer}#scrolltop:hover{text-decoration:none}#scrolltop a{display:block;width:30px;height:24px;padding:3px 5px;line-height:12px;text-align:center;color:#787878;text-decoration:none;background:url() no-repeat 0 0;border-top:1px #cdcdcd solid}a.scrolltopa:hover{background-position:-40px 0px!important}#scrolltop a b{visibility:hidden;font-weight:normal}.bm{margin-bottom:10px}.bm{border:1px solid #cdcdcd;background:#FFF;margin:8px 4px 8px 0;border-radius:4px;box-shadow:1px 1px 2px #666}.bm_c{padding:10px}.bm_h{padding:0 10px;height:31px;border-top:1px solid #FFF;border-bottom:1px solid #c2d5e3;background:#f2f2f2;line-height:31px;white-space:nowrap;overflow:hidden;border-radius:5px 5px 0 0}.xl li{margin:2px 0}.xl em{float:right;padding-left:5px}.xl em{color:#999}.xl1 li{overflow:hidden}.xl2 li{float:left;height:1.5em;overflow:hidden}.p_pop{padding:4px;border:1px solid;min-width:60px;border-color:#DDD;background:#fefefe;box-shadow:1px 2px 2px rgba(0,0,0,0.3)}.p_pop li{display:inline}.h_pop{border-top:0}.block .xl1 ul li{padding-left:10px;background:url() no-repeat 0 50%;border-bottom:dotted 1px #DDD;margin-bottom:6px}#footer{background:none repeat scroll 0 0 #555;padding-top:1.5em;margin:2em 0 0;border-top:2px solid #AAA;color:#CCC;text-align:center}#footer a{color:#CCC;border:0 none}#footer a:hover{color:#FFF}.copyright{color:#999;line-height:1.5em;font-size:12px;border:1px #EEE solid;-moz-border-radius:5px;-webkit-border-radius:5px;-o-border-radius:5px;-ms-border-radius:5px;border-radius:5px}.copyright a{color:#333}.copyright{background-color:rgba(24,241,24,0.1);padding:5px 10px;margin:5px;text-align:left}.clear{clear:both}.vw .h{padding:15px;background-color:#FFF;border-radius:4px;border-bottom:1px solid #cdcdcd;border-right:1px solid #cdcdcd}.vw .ph{color:#000;font-size:32px;text-shadow:2px 2px 4px #fff,-2px 2px 4px #fff,2px -2px 4px #fff,-2px -2px 4px #fff}.vw .d{min-height:100px;font-size:16px;line-height:2em;color:#000}.vw .d p em{color:#42962a;font-style:normal}.vw .d a{color:#f76e6e;text-decoration:none;padding:0 4px 2px 4px;border-bottom:2px solid #eee}.vw .d a:hover{border-bottom:2px solid #999}.vw .d a.ext{color:#4d8ad8}.vw .d img{max-width:95%;text-align:center;border:0;box-shadow:1px 1px 4px #666;margin:10px 5px}.vw .d p{line-height:2em;margin:1.5em 0}#article_content{vertical-align:top;min-height:400px;background-color:#FFF;padding:15px;border-bottom:1px solid #cdcdcd;border-right:1px solid #cdcdcd;box-shadow:1px 1px 2px #666}#article_content .article_img,#article_content .article_img_desc{text-align:center;margin:0}#article_content .article_img{margin:5px}#article_content .article_img_desc em{min-width:20%;min-height:22px;display:inline-block;padding:10px;margin:0 auto 10px auto;border-bottom:1px solid #d9d9d9;font-size:13px;color:#999;font-style:italic;line-height:1.7}.vw .d ol{margin:1em 1em 1em 2em}.vw .d ol li{list-style-type:decimal;line-height:2em}.vw .o{margin-top:15px;padding-top:5px;border-top:1px solid #cdcdcd;text-align:right}#nv_portal .vw .o,#nv_portal .vw .pren{margin-right:15px}#nv_portal .vw .h{margin-bottom:5px;box-shadow:1px 1px 2px #666;border:0}#nv_portal .vw .pren em{display:block;width:100%;height:22px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.wp{width:100%;max-width:1200px;min-width:1200px}.block .xl1 ul li{height:25px}#footer{padding-bottom:1.5em}#nv_portal.pg_view .ct2 .mn{width:100%}#nv_portal.pg_view .ct2 .mn .d{float:left}#nv_portal .vw .d,#nv_portal .vw .bm{margin-right:1px}#nv_portal #article_copyright{font-size:12px!important;padding:0;position:absolute;bottom:0;text-align:center;width:100%;text-shadow:1px 1px 2px #fff,-1px 1px 2px #fff,1px -1px 2px #fff,-1px -1px 2px #fff}#nv_portal #article_date{display:inline}#nv_portal .vw #translator_info{width:350px}#nv_portal .vw #translator_info{font-size:14px;line-height:2em}#nv_portal .vw #translator_info a{border-bottom:0;padding:0}#nv_portal .vw #footer_info{margin-right:1px}#nv_portal .vw #related{width:350px;float:right}#nv_portal .vw .d h3{display:block;font-weight:300;background-image:url();background-position:top;padding-top:32px;background-size:32px;background-repeat:no-repeat;font-size:1.4em;line-height:2em;margin:24px 10px;letter-spacing:2px;text-shadow:1px 1px 1px #666;text-align:center}#nv_portal #add_comment,#nv_portal #comment{padding:0}#topmenu.affix{position:fixed;top:0px;z-index:1000;opacity:.9}@-ms-viewport{width:device-width}.tsina,.wxicon,.tticon,.zhicon,.wyicon,.fbicon,.twicon,.tgicon,.favorite,.print{display:inline-block;width:16px;height:16px;margin-bottom:-3px;margin-right:6px;overflow:hidden;opacity:1.0}.tsina{background:url() no-repeat center center!important}.wxicon{background:url() no-repeat center center!important}.tticon{background:url() no-repeat center center!important}.zhicon{background:url() no-repeat center center!important}.wyicon{background:url() no-repeat center center!important}.fbicon{background:url() no-repeat center center!important}.twicon{background:url() no-repeat center center!important}.tgicon{background:url() no-repeat center center!important}.favorite{background:url() no-repeat center center!important}.print{background:url() no-repeat center center!important}a.tqq:hover,a.tsina:hover,a.tqq_large:hover,a.tsina_large:hover,a.weixin_large:hover,a.fav_large:hover,a.print_large:hover,a.qricon:hover,a.wxicon:hover,a.tticon:hover,.zhicon:hover,.wyicon:hover,.fbicon:hover,.twicon:hover,.tgicon:hover,a.siteshare:hover,a.favorite:hover,a.invite:hover{border-bottom:0;opacity:.5}#qrcode p{font-weight:bold;font-size:14px;text-align:center}#qrcode #canvas{width:200px;height:200px;margin:0 15px}#related{margin:8px 4px 8px 0!important}#related_tag ul#tag li{background:url() no-repeat 4px 6px #EEE;border:solid 1px #CCC;padding:4px 4px 4px 28px;margin:1px 4px;border-radius:4px;-webkit-border-radius:4px;-moz-border-radius:4px;width:35%}#related_tag ul#tag li a{font-weight:normal;font-size:12px;font-family:monospace}.mutag{position:relative}.mutag li{float:left;height:40px;line-height:35px}.mutag a{float:left;display:inline;margin:10px;padding:4px;white-space:nowrap;background:#EEE;line-height:2em;border:1px solid #CCC;background-color:#fff!important;border-color:#ccc #fff #fff #ccc}.mutag a:hover{border:1px solid #c2d5e3;background-color:#e5edf2;color:#369;text-decoration:none}@media (max-width:480px){#topmenu_wrapper{overflow:hidden}#mn_N6666{display:block!important}#nv li{width:65px}#scbar{display:none}#mu{display:none}#wp .wp{width:100%;max-width:100%;min-width:100%}.block .xl1 ul li{font-size:14px}.wp{width:100%;max-width:100%;min-width:100%}.wp h1{display:none!important}.wp .vw h1{display:block!important}.bm{margin:0}#nv_portal .vw .d{margin-right:0!important}#nv_portal .ct2 .mn{width:100%;float:none;margin-bottom:0}#nv_portal.pg_view .ct2 .mn .d{float:none}#nv_portal .vw .h,#nv_portal .vw .d{margin-right:0}#nv_portal #article_date{display:none}#nv_portal .vw #footer_info{width:92%;margin:4px 0}#nv_portal .vw #related,#nv_portal .vw #translator_info{width:100%}#nv_portal .vw .d img{max-width:95%;margin:auto}#nv_portal #scrolltop{display:none}#nv_portal .vw .ph{font-size:18px!important}#nv_portal .vw .h.hm{height:200px!important}#nv_portal .vw .h.hm h1{width:80%!important}#nv_portal #translator_info{margin-right:4px}#nv_portal #article_content{border:0}#nv_portal .vw .bm{border:0;border-radius:0;box-shadow:none}}</style>
<link rel=alternate type=application/rss+xml title="Linux 中国◆开源社区 RSS" href=https://linux.cn/rss.xml>
<link rel=canonical href=https://linux.cn/article-9703-1.html>
<link rel="shortcut icon" href="" sizes=32x32><style>.sf-hidden{display:none!important}</style></head>
<body id=nv_portal class=pg_view>
<div id=append_parent></div><div id=ajaxwaitid class=sf-hidden></div>

<div id=wp class=wp>
<style>#nv_portal .vw .h.hm{background:#FFF url();border-radius:0;background-size:cover;background-position:center;height:640px;position:relative;padding:10px;margin-bottom:0}#nv_portal .vw .h.hm h1{background-color:rgba(33,33,33,0.8);border-radius:0;position:absolute;margin:10px 20px 20px 20px;padding:15px 10px;bottom:0;width:90%}</style>
<div id=ct class="ct2 wp cl">
<div class=mn>
<article class=vw>

<div class=d style=width:100%;margin:0>
<div id=article_content style=border-radius:0 class>
<p class>在上一篇<a href=https://linux.cn/article-9700-1.html>文章</a>中，我们展示了如何创建一个 systemd 服务并使普通用户可以启动和终止游戏服务器。但到目前为止，使用这个服务并不比直接运行服务器高明多少。让我们更进一步，让其可以向玩家发邮件，包括在服务器可用时通知玩家，在服务器关闭前警告玩家：</p>
<pre class="prettyprint linenums prettyprinted"><ol class=linenums><li class=L0><code><span class=com>#</span><span class=pln> minetest</span><span class=pun>.</span><span class=pln>service</span></code><li class=L1><code></code><li class=L2><code><span class=pun>[</span><span class=typ>Unit</span><span class=pun>]</span></code><li class=L3><code><span class=typ>Description</span><span class=pun>=</span><span class=pln> </span><span class=typ>Minetest</span><span class=pln> server</span></code><li class=L4><code><span class=typ>Documentation</span><span class=pun>=</span><span class=pln> https</span><span class=pun>:</span><span class=com>//wiki.minetest.net/Main_Page</span></code><li class=L5><code></code><li class=L6><code><span class=pun>[</span><span class=typ>Service</span><span class=pun>]</span></code><li class=L7><code><span class=typ>Type</span><span class=pun>=</span><span class=pln> simple</span></code><li class=L8><code></code><li class=L9><code><span class=typ>ExecStart</span><span class=pun>=</span><span class=pln> </span><span class=str>/usr/</span><span class=pln>games</span><span class=pun>/</span><span class=pln>minetest </span><span class=pun>--</span><span class=pln>server</span></code><li class=L0><code><span class=typ>ExecStartPost</span><span class=pun>=</span><span class=pln> </span><span class=str>/home/</span><span class=pun>&lt;</span><span class=pln>username</span><span class=pun>&gt;</span><span class=str>/bin/</span><span class=pln>mtsendmail</span><span class=pun>.</span><span class=pln>sh </span><span class=str>"Ready to rumble?"</span><span class=pln> </span><span class=str>"Minetest Starting up"</span></code><li class=L1><code></code><li class=L2><code><span class=typ>TimeoutStopSec</span><span class=pun>=</span><span class=pln> </span><span class=lit>180</span></code><li class=L3><code><span class=typ>ExecStop</span><span class=pun>=</span><span class=pln> </span><span class=str>/home/</span><span class=pun>&lt;</span><span class=pln>username</span><span class=pun>&gt;</span><span class=str>/bin/</span><span class=pln>mtsendmail</span><span class=pun>.</span><span class=pln>sh </span><span class=str>"Off to bed. Nightie night!"</span><span class=pln> </span><span class=str>"</span></code><li class=L4><code><span class=str>&nbsp; Minetest Stopping in 2 minutes"</span></code><li class=L5><code><span class=typ>ExecStop</span><span class=pun>=</span><span class=pln> </span><span class=str>/bin/</span><span class=kwd>sleep</span><span class=pln> </span><span class=lit>120</span></code><li class=L6><code><span class=typ>ExecStop</span><span class=pun>=</span><span class=pln> </span><span class=str>/bin/</span><span class=kwd>kill</span><span class=pln> </span><span class=pun>-</span><span class=lit>2</span><span class=pln> $MAINPID</span></code></ol></pre>
<p class>这里涉及几个新的指令。首先是 <code>ExecStartPost</code> 指令，该指令可以在主进程启动后马上执行任何你指定的操作。在本例中，你执行了一个自定义脚本 <code>mtsendmail</code> (内容如下)，该脚本以邮件形式通知你的朋友服务器已经启动。</p>
<pre class="prettyprint linenums prettyprinted"><ol class=linenums><li class=L0><code><span class=com>#</span><span class=pun>!</span><span class=str>/bin/</span><span class=kwd>bash</span></code><li class=L1><code><span class=com>#</span><span class=pln> mtsendmail</span></code><li class=L2><code><span class=kwd>echo</span><span class=pln> $1 </span><span class=pun>|</span><span class=pln> mutt </span><span class=pun>-</span><span class=pln>F </span><span class=pun>/</span><span class=pln>home</span><span class=pun>/&lt;</span><span class=pln>username</span><span class=pun>&gt;/.</span><span class=pln>muttrc </span><span class=pun>-</span><span class=pln>s </span><span class=str>"$2"</span><span class=pln> my_minetest@mailing_list</span><span class=pun>.</span><span class=pln>com</span></code></ol></pre>
<p>我们使用 <a href=http://www.mutt.org/ class=ext rel="external nofollow" target=_blank>Mutt</a> 这个命令后邮件客户端发送消息。虽然从实际效果来看，上述脚本仅有 1 行，但 systemd 单元的参数中不能包含管道及重定向操作，故我们需要将其封装到脚本中。</p>
<p>顺便提一下，还有一个 <code>ExecStartPre</code> 指令，用于在服务主进程执行之前进行指定操作。</p>
<p>接下来我们看到，关闭服务器涉及了好几条指令。<code>TimeoutStopSec</code> 指令用于设置 systemd 友好关闭服务的最大等候时间，默认值大约是 90 秒。超过这个最大等候时间，systemd 会强制关闭服务并报错。考虑到你希望在彻底关闭服务器前给用户预留几分钟的时间，你需要将超时时间提高至 3 分钟，这样 systemd 就不会误认为服务关闭时出现问题。</p>
<p>接下来是关闭服务的具体指令部分。虽然没有 <code>ExecStopPre</code> 这样的指令，但你可以通过多次使用 <code>ExecStop</code> 指令实现关闭服务器前执行操作的目标。多个 <code>ExecStop</code> 指令按从上到下的顺序依次运行，这样你就可以在服务器真正关闭前向用户发送消息。</p>
<p>通过这个特性，你首先应该向你的朋友发邮件，警告其服务器即将关闭，然后等待两分钟，最后关闭服务器。可以使用 <code>Ctrl + c</code> 关闭 Minetest 服务器，该操作会被转换为一个中断信号（<code>SIGINT</code>）；当你执行 <code>kill -2 $MAINPID</code> 时就会发送该中断信号，其中 <code>$MAINPID</code> 是 systemd 变量，用于记录你服务中主进程的 PID 信息。</p>
<p>看上去好多了！如果你此时启动服务：</p>
<pre class="prettyprint linenums prettyprinted"><ol class=linenums><li class=L0><code><span class=kwd>systemctl</span><span class=pln> </span><span class=pun>--</span><span class=pln>user start minetest</span></code></ol></pre>
<p>服务会启动 Minetest 服务器并向你的用户发送邮件。关闭服务的情形基本类似，只不过会额外留给用户 2 分钟时间退出登录。</p>
<h3 id=toc_1 class>开机自启动</h3>
<p>下一步我们让你的服务在主机启动后立即可用，在主机关闭时自动关闭。</p>
<p>我们需要将你的服务文件移动到系统服务目录，即 <code>/etc/systemd/system/</code>：</p>
<pre class="prettyprint linenums prettyprinted"><ol class=linenums><li class=L0><code><span class=kwd>sudo</span><span class=pln> </span><span class=kwd>mv</span><span class=pln> </span><span class=pun>/</span><span class=pln>home</span><span class=pun>/&lt;</span><span class=pln>username</span><span class=pun>&gt;</span><span class=str>/.config/</span><span class=kwd>systemd</span><span class=pun>/</span><span class=pln>user</span><span class=pun>/</span><span class=pln>minetest</span><span class=pun>.</span><span class=pln>service </span><span class=pun>/</span><span class=pln>etc</span><span class=pun>/</span><span class=kwd>systemd</span><span class=pun>/</span><span class=pln>system</span><span class=pun>/</span></code></ol></pre>
<p>如果你希望此时启动该服务，你需要拥有超级用户权限：</p>
<pre class="prettyprint linenums prettyprinted"><ol class=linenums><li class=L0><code><span class=kwd>sudo</span><span class=pln> </span><span class=kwd>systemctl</span><span class=pln> start minetest</span></code></ol></pre>
<p>另外，可以使用如下命令检查服务状态：</p>
<pre class="prettyprint linenums prettyprinted"><ol class=linenums><li class=L0><code><span class=kwd>sudo</span><span class=pln> </span><span class=kwd>systemctl</span><span class=pln> status minetest</span></code></ol></pre>
<p>你会发现服务很糟糕地处于失败状态，这是因为 systemd 不能通过上下文信息、特征、配置文件得知具体使用哪个用户运行该服务。在单元文件中增加 <code>User</code> 指令可以解决这个问题。</p>
<pre class="prettyprint linenums prettyprinted"><ol class=linenums><li class=L0><code><span class=com>#</span><span class=pln> minetest</span><span class=pun>.</span><span class=pln>service</span></code><li class=L1><code></code><li class=L2><code><span class=pun>[</span><span class=typ>Unit</span><span class=pun>]</span></code><li class=L3><code><span class=typ>Description</span><span class=pun>=</span><span class=pln> </span><span class=typ>Minetest</span><span class=pln> server</span></code><li class=L4><code><span class=typ>Documentation</span><span class=pun>=</span><span class=pln> https</span><span class=pun>:</span><span class=com>//wiki.minetest.net/Main_Page</span></code><li class=L5><code></code><li class=L6><code><span class=pun>[</span><span class=typ>Service</span><span class=pun>]</span></code><li class=L7><code><span class=typ>Type</span><span class=pun>=</span><span class=pln> simple</span></code><li class=L8><code><span class=typ>User</span><span class=pun>=</span><span class=pln> </span><span class=pun>&lt;</span><span class=pln>username</span><span class=pun>&gt;</span></code><li class=L9><code></code><li class=L0><code><span class=typ>ExecStart</span><span class=pun>=</span><span class=pln> </span><span class=str>/usr/</span><span class=pln>games</span><span class=pun>/</span><span class=pln>minetest </span><span class=pun>--</span><span class=pln>server</span></code><li class=L1><code><span class=typ>ExecStartPost</span><span class=pun>=</span><span class=pln> </span><span class=str>/home/</span><span class=pun>&lt;</span><span class=pln>username</span><span class=pun>&gt;</span><span class=str>/bin/</span><span class=pln>mtsendmail</span><span class=pun>.</span><span class=pln>sh </span><span class=str>"Ready to rumble?"</span></code><li class=L2><code><span class=pln>&nbsp; </span><span class=str>"Minetest Starting up"</span></code><li class=L3><code></code><li class=L4><code><span class=typ>TimeoutStopSec</span><span class=pun>=</span><span class=pln> </span><span class=lit>180</span></code><li class=L5><code><span class=typ>ExecStop</span><span class=pun>=</span><span class=pln> </span><span class=str>/home/</span><span class=pun>&lt;</span><span class=pln>username</span><span class=pun>&gt;</span><span class=str>/bin/</span><span class=pln>mtsendmail</span><span class=pun>.</span><span class=pln>sh </span><span class=str>"Off to bed. Nightie night!"</span></code><li class=L6><code><span class=pln>&nbsp; </span><span class=str>"Minetest Stopping in 2 minutes"</span></code><li class=L7><code><span class=typ>ExecStop</span><span class=pun>=</span><span class=pln> </span><span class=str>/bin/</span><span class=kwd>sleep</span><span class=pln> </span><span class=lit>120</span></code><li class=L8><code><span class=typ>ExecStop</span><span class=pun>=</span><span class=pln> </span><span class=str>/bin/</span><span class=kwd>kill</span><span class=pln> </span><span class=pun>-</span><span class=lit>2</span><span class=pln> $MAINPID</span></code><li class=L9><code></code></ol></pre>
<p>systemd 从 <code>User</code> 指令中得知应使用哪个用户的环境变量来正确运行该服务。你可以使用 root 用户，但这可能产生安全风险；使用你的个人用户会好一些，但不少管理员的做法是为服务单独创建一个用户，这样可以有效地将服务与其它用户和系统组件相互隔离。</p>
<p>下一步我们让你的服务在系统启动时自动启动，系统关闭时自动关闭。要达到这个目的，你需要 <em>启用</em> 你的服务；但在这之前，你还需要告知 systemd 从哪里 <em>安装</em> 它。</p>
<p>对于 systemd 而言，<em>安装</em> 意味着告知 systemd 在系统启动的具体哪个步骤激活你的服务。以通用 Unix 打印系统（<code>cups.service</code>）为例，它的启动在网络框架启动之后、其它打印服务启动之前。又如，<code>minetest.server</code> 需要使用用户邮件（及其它组件），需要等待网络和普通用户对应的服务就绪后才可启动。</p>
<p>你只需要在单元文件中添加一个新段和新指令：</p>
<pre class="prettyprint linenums prettyprinted"><ol class=linenums><li class=L0><code><span class=pun>...</span></code><li class=L1><code><span class=pun>[</span><span class=typ>Install</span><span class=pun>]</span></code><li class=L2><code><span class=typ>WantedBy</span><span class=pun>=</span><span class=pln> multi</span><span class=pun>-</span><span class=pln>user</span><span class=pun>.</span><span class=pln>target</span></code></ol></pre>
<p>你可以将其理解为“等待多用户系统的全部内容就绪”。systemd 中的“目标”类似于旧系统中的运行级别，可以用于将主机转移到一个或另一个状态，也可以像本例中这样让你的服务等待指定状态出现后运行。</p>
<p>你的最终 <code>minetest.service</code> 文件如下：</p>
<pre class="prettyprint linenums prettyprinted"><ol class=linenums><li class=L0><code><span class=com>#</span><span class=pln> minetest</span><span class=pun>.</span><span class=pln>service</span></code><li class=L1><code><span class=pun>[</span><span class=typ>Unit</span><span class=pun>]</span></code><li class=L2><code><span class=typ>Description</span><span class=pun>=</span><span class=pln> </span><span class=typ>Minetest</span><span class=pln> server</span></code><li class=L3><code><span class=typ>Documentation</span><span class=pun>=</span><span class=pln> https</span><span class=pun>:</span><span class=com>//wiki.minetest.net/Main_Page</span></code><li class=L4><code></code><li class=L5><code><span class=pun>[</span><span class=typ>Service</span><span class=pun>]</span></code><li class=L6><code><span class=typ>Type</span><span class=pun>=</span><span class=pln> simple</span></code><li class=L7><code><span class=typ>User</span><span class=pun>=</span><span class=pln> </span><span class=pun>&lt;</span><span class=pln>username</span><span class=pun>&gt;</span></code><li class=L8><code></code><li class=L9><code><span class=typ>ExecStart</span><span class=pun>=</span><span class=pln> </span><span class=str>/usr/</span><span class=pln>games</span><span class=pun>/</span><span class=pln>minetest </span><span class=pun>--</span><span class=pln>server</span></code><li class=L0><code><span class=typ>ExecStartPost</span><span class=pun>=</span><span class=pln> </span><span class=str>/home/</span><span class=pun>&lt;</span><span class=pln>username</span><span class=pun>&gt;</span><span class=str>/bin/</span><span class=pln>mtsendmail</span><span class=pun>.</span><span class=pln>sh </span><span class=str>"Ready to rumble?"</span></code><li class=L1><code><span class=pln>&nbsp; </span><span class=str>"Minetest Starting up"</span></code><li class=L2><code></code><li class=L3><code><span class=typ>TimeoutStopSec</span><span class=pun>=</span><span class=pln> </span><span class=lit>180</span></code><li class=L4><code><span class=typ>ExecStop</span><span class=pun>=</span><span class=pln> </span><span class=str>/home/</span><span class=pun>&lt;</span><span class=pln>username</span><span class=pun>&gt;</span><span class=str>/bin/</span><span class=pln>mtsendmail</span><span class=pun>.</span><span class=pln>sh </span><span class=str>"Off to bed. Nightie night!"</span></code><li class=L5><code><span class=pln>&nbsp; </span><span class=str>"Minetest Stopping in 2 minutes"</span></code><li class=L6><code><span class=typ>ExecStop</span><span class=pun>=</span><span class=pln> </span><span class=str>/bin/</span><span class=kwd>sleep</span><span class=pln> </span><span class=lit>120</span></code><li class=L7><code><span class=typ>ExecStop</span><span class=pun>=</span><span class=pln> </span><span class=str>/bin/</span><span class=kwd>kill</span><span class=pln> </span><span class=pun>-</span><span class=lit>2</span><span class=pln> $MAINPID</span></code><li class=L8><code></code><li class=L9><code><span class=pun>[</span><span class=typ>Install</span><span class=pun>]</span></code><li class=L0><code><span class=typ>WantedBy</span><span class=pun>=</span><span class=pln> multi</span><span class=pun>-</span><span class=pln>user</span><span class=pun>.</span><span class=pln>target</span></code><li class=L1><code></code></ol></pre>
<p>在尝试新的服务之前，你还需要对邮件脚本做一些调整：</p>
<pre class="prettyprint linenums prettyprinted"><ol class=linenums><li class=L0><code><span class=com>#</span><span class=pun>!</span><span class=str>/bin/</span><span class=kwd>bash</span></code><li class=L1><code><span class=com>#</span><span class=pln> mtsendmail</span></code><li class=L2><code></code><li class=L3><code><span class=kwd>sleep</span><span class=pln> </span><span class=lit>20</span></code><li class=L4><code><span class=kwd>echo</span><span class=pln> $1 </span><span class=pun>|</span><span class=pln> mutt </span><span class=pun>-</span><span class=pln>F </span><span class=pun>/</span><span class=pln>home</span><span class=pun>/&lt;</span><span class=pln>username</span><span class=pun>&gt;/.</span><span class=pln>muttrc </span><span class=pun>-</span><span class=pln>s </span><span class=str>"$2"</span><span class=pln> my_minetest@mailing_list</span><span class=pun>.</span><span class=pln>com</span></code><li class=L5><code><span class=kwd>sleep</span><span class=pln> </span><span class=lit>10</span></code></ol></pre>
<p>这是因为系统需要一定的时间启动邮件系统（这里等待 20 秒），也需要一定时间完成邮件发送（这里等待 10 秒）。注意脚本中的等待时间数值适用于我的系统，你可能需要针对你的系统调整数值。</p>
<p>大功告成啦。执行如下操作：</p>
<pre class="prettyprint linenums prettyprinted"><ol class=linenums><li class=L0><code><span class=kwd>sudo</span><span class=pln> </span><span class=kwd>systemctl</span><span class=pln> enable minetest</span></code></ol></pre>
<p class>你的 Minetest 服务将在系统启动时自动启动，在系统关闭时友好关闭并通知你的用户。</p>









</div>
</div>
<br class=clear>
<div class=vw>
<div id=add_comment class="h bm" style=display:none>
<div class="bm_h cl">
</div>
<div class="bm_c pbn">
</div>
</div>
<div id=comment class="bm h">


</div>
</div>

<div>
</div>

<br class=clear>
</article>
</div>
</div>
<style>code .str{color:#65b042}code .kwd{color:#e28964}code .com{color:#aeaeae;font-style:italic}code .typ{color:#89bdff}code .lit{color:#3387cc}code .pun{color:#b8ffb8}code .pln{color:#b8ffb8}pre,code,ol.linenums{background-color:#161b20;background-image:url("")}pre:hover,pre li:hover,pre code:hover{background-color:#111}pre{box-shadow:0 0 5px rgba(0,0,0,0.5);text-shadow:0 1px 1px #000;border-radius:6px;-moz-border-radius:6px;-webkit-border-radius:6px;-o-border-radius:6px;-ms-border-radius:6px;color:#b8ffb8;margin:10px;padding:1em 1em 1em 2em;white-space:pre-wrap}pre.prettyprint{font-size:14px;border:1px solid #000}pre ol{margin:1em!important;border-left:2px solid #090;padding:1px}pre li:hover{cursor:text}pre code{background:0;border:none!important;padding:0 0 0 10px!important;border-radius:0!important;margin:1em 0!important}code{padding:2px 4px;border-radius:4px;-moz-border-radius:4px;-webkit-border-radius:4px;-o-border-radius:4px;-ms-border-radius:4px;margin:0 4px;color:#2196f3;background:#EEE}code:hover{color:#2196f3}ol.linenums{margin-top:0;margin-bottom:0;color:#aeaeae}ol.linenums li{line-height:1.5em!important;text-indent:-28px;padding-left:28px}</style>
 </div>
 

<div id=scrolltop style=left:auto;right:0px;visibility:visible>
<span>



 
</span>
<span hidefocus=true></span>
</div>
<div id=qrcode class=sf-hidden>
<p>分享到微信</p>
<div id=canvas></div>
<p></p></div>
<script data-template-shadow-root>(() => { document.currentScript.remove(); const processNode = node => { node.querySelectorAll("template[shadowmode]").forEach(element=>{ if (!element.parentElement.shadowRoot) { const shadowRoot = element.parentElement.attachShadow({mode:element.getAttribute("shadowmode"),delegatesFocus:Boolean(element.getAttribute("delegatesfocus"))}); shadowRoot.innerHTML = element.innerHTML; element.remove(); processNode(shadowRoot); } }) }; const FORBIDDEN_TAG_NAMES = ["a","area","audio","base","br","col","command","embed","hr","img","iframe","input","keygen","link","meta","param","source","track","video","wbr"]; const NOTE_TAGNAME = "single-file-note"; const NOTE_CLASS = "note"; const NOTE_ANCHORED_CLASS = "note-anchored"; const NOTE_SELECTED_CLASS = "note-selected"; const NOTE_MOVING_CLASS = "note-moving"; const NOTE_MASK_MOVING_CLASS = "note-mask-moving"; const MASK_CLASS = "single-file-mask"; const HIGHLIGHT_CLASS = "single-file-highlight"; const NOTES_WEB_STYLESHEET = ".note { all: initial; display: flex; flex-direction: column; height: 150px; width: 150px; position: absolute; top: 10px; left: 10px; border: 1px solid rgb(191, 191, 191); z-index: 2147483646; box-shadow: 2px 2px 5px rgba(33, 33, 33, .7); min-height: 100px; min-width: 100px; } .note-selected { z-index: 2147483647; } .note-hidden { display: none; } .note-collapsed { min-height: 30px; max-height: 30px; overflow: hidden; } .note textarea { all: initial; white-space: pre; font-family: Arial, Helvetica, sans-serif; font-size: 14px; padding: 3px; height: 100%; border: 1px solid transparent; resize: none; } .note textarea:focus { border: 1px dotted rgb(160, 160, 160); } .note header { all: initial; min-height: 30px; cursor: grab; user-select: none; } .note .note-remove { all: initial; position: absolute; right: 0px; top: 2px; padding: 5px; opacity: .5; cursor: pointer; user-select: none; } .note .note-anchor { all: initial; position: absolute; left: 0px; top: 2px; padding: 5px; opacity: .25; cursor: pointer; } .note .note-resize { all: initial; position: absolute; bottom: -5px; right: -5px; height: 15px; width: 15px; cursor: nwse-resize; user-select: none; } .note .note-remove:hover { opacity: 1; } .note .note-anchor:hover { opacity: .5; } .note-anchored .note-anchor { opacity: .5; } .note-anchored .note-anchor:hover { opacity: 1; } .note-moving { opacity: .5; } .note-moving * { cursor: grabbing; } .note-yellow header { background-color: #f5f545; } .note-yellow textarea { background-color: #ffff7c; } .note-pink header { background-color: #ffa59f; } .note-pink textarea { background-color: #ffbbb6; } .note-blue header { background-color: #84c8ff; } .note-blue textarea { background-color: #95d0ff; } .note-green header { background-color: #93ef8d; } .note-green textarea { background-color: #9cff95; }"; const MASK_WEB_STYLESHEET = ".note-mask { all: initial; position: fixed; z-index: 2147483645; pointer-events: none; } .note-mask-moving { background-color: rgba(236, 234, 234, 0.5); } .page-mask { all: initial; position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483646; } .page-mask-active { width: 100vw; height: 100vh; }"; const NOTE_HEADER_HEIGHT = 25; const PAGE_MASK_ACTIVE_CLASS = "page-mask-active"; const REMOVED_CONTENT_CLASS = "single-file-removed"; const reflowNotes = function reflowNotes() { document.querySelectorAll(NOTE_TAGNAME).forEach(containerElement => { const noteElement = containerElement.shadowRoot.querySelector("." + NOTE_CLASS); const noteBoundingRect = noteElement.getBoundingClientRect(); const anchorElement = getAnchorElement(containerElement); const anchorBoundingRect = anchorElement.getBoundingClientRect(); const maxX = anchorBoundingRect.x + Math.max(0, anchorBoundingRect.width - noteBoundingRect.width); const minX = anchorBoundingRect.x; const maxY = anchorBoundingRect.y + Math.max(0, anchorBoundingRect.height - NOTE_HEADER_HEIGHT); const minY = anchorBoundingRect.y; let left = parseInt(noteElement.style.getPropertyValue("left")); let top = parseInt(noteElement.style.getPropertyValue("top")); if (noteBoundingRect.x > maxX) { left -= noteBoundingRect.x - maxX; } if (noteBoundingRect.x < minX) { left += minX - noteBoundingRect.x; } if (noteBoundingRect.y > maxY) { top -= noteBoundingRect.y - maxY; } if (noteBoundingRect.y < minY) { top += minY - noteBoundingRect.y; } noteElement.style.setProperty("position", "absolute"); noteElement.style.setProperty("left", left + "px"); noteElement.style.setProperty("top", top + "px"); }); }; const addNoteRef = function addNoteRef(anchorElement, noteId) { const noteRefs = getNoteRefs(anchorElement); noteRefs.push(noteId); setNoteRefs(anchorElement, noteRefs); }; const deleteNoteRef = function deleteNoteRef(containerElement, noteId) { const anchorElement = getAnchorElement(containerElement); const noteRefs = getNoteRefs(anchorElement).filter(noteRefs => noteRefs != noteId); if (noteRefs.length) { setNoteRefs(anchorElement, noteRefs); } else { delete anchorElement.dataset.singleFileNoteRefs; } }; const getNoteRefs = function getNoteRefs(anchorElement) { return JSON.parse("[" + (anchorElement.dataset.singleFileNoteRefs || "") + "]"); }; const setNoteRefs = function setNoteRefs(anchorElement, noteRefs) { anchorElement.dataset.singleFileNoteRefs = noteRefs.toString(); }; const getAnchorElement = function getAnchorElement(containerElement) { return document.querySelector("[data-single-file-note-refs^=" + JSON.stringify(containerElement.dataset.noteId) + "], [data-single-file-note-refs$=" + JSON.stringify(containerElement.dataset.noteId) + "], [data-single-file-note-refs*=" + JSON.stringify("," + containerElement.dataset.noteId + ",") + "]"); }; const getMaskElement = function getMaskElement(className, containerClassName) { let maskElement = document.documentElement.querySelector("." + className); if (!maskElement) { maskElement = document.createElement("div"); const maskContainerElement = document.createElement("div"); if (containerClassName) { maskContainerElement.classList.add(containerClassName); } maskContainerElement.classList.add(MASK_CLASS); const firstNote = document.querySelector(NOTE_TAGNAME); if (firstNote && firstNote.parentElement == document.documentElement) { document.documentElement.insertBefore(maskContainerElement, firstNote); } else { document.documentElement.appendChild(maskContainerElement); } maskElement.classList.add(className); const maskShadow = maskContainerElement.attachShadow({ mode: "open" }); maskShadow.appendChild(getStyleElement(MASK_WEB_STYLESHEET)); maskShadow.appendChild(maskElement); return maskElement; } }; const getStyleElement = function getStyleElement(stylesheet) { const linkElement = document.createElement("style"); linkElement.textContent = stylesheet; return linkElement; }; const attachNoteListeners = function attachNoteListeners(containerElement, editable = false) { const SELECT_PX_THRESHOLD = 4; const COLLAPSING_NOTE_DELAY = 750; const noteShadow = containerElement.shadowRoot; const noteElement = noteShadow.childNodes[1]; const headerElement = noteShadow.querySelector("header"); const mainElement = noteShadow.querySelector("textarea"); const noteId = containerElement.dataset.noteId; const resizeElement = noteShadow.querySelector(".note-resize"); const anchorIconElement = noteShadow.querySelector(".note-anchor"); const removeNoteElement = noteShadow.querySelector(".note-remove"); mainElement.readOnly = !editable; if (!editable) { anchorIconElement.style.setProperty("display", "none", "important"); } else { anchorIconElement.style.removeProperty("display"); } headerElement.ontouchstart = headerElement.onmousedown = event => { if (event.target == headerElement) { collapseNoteTimeout = setTimeout(() => noteElement.classList.toggle("note-collapsed"), COLLAPSING_NOTE_DELAY); event.preventDefault(); const position = getPosition(event); const clientX = position.clientX; const clientY = position.clientY; const boundingRect = noteElement.getBoundingClientRect(); const deltaX = clientX - boundingRect.left; const deltaY = clientY - boundingRect.top; maskPageElement.classList.add(PAGE_MASK_ACTIVE_CLASS); document.documentElement.style.setProperty("user-select", "none", "important"); anchorElement = getAnchorElement(containerElement); displayMaskNote(); selectNote(noteElement); moveNote(event, deltaX, deltaY); movingNoteMode = { event, deltaX, deltaY }; document.documentElement.ontouchmove = document.documentElement.onmousemove = event => { clearTimeout(collapseNoteTimeout); if (!movingNoteMode) { movingNoteMode = { deltaX, deltaY }; } movingNoteMode.event = event; moveNote(event, deltaX, deltaY); }; } }; resizeElement.ontouchstart = resizeElement.onmousedown = event => { event.preventDefault(); resizingNoteMode = true; selectNote(noteElement); maskPageElement.classList.add(PAGE_MASK_ACTIVE_CLASS); document.documentElement.style.setProperty("user-select", "none", "important"); document.documentElement.ontouchmove = document.documentElement.onmousemove = event => { event.preventDefault(); const { clientX, clientY } = getPosition(event); const boundingRectNote = noteElement.getBoundingClientRect(); noteElement.style.width = clientX - boundingRectNote.left + "px"; noteElement.style.height = clientY - boundingRectNote.top + "px"; }; }; anchorIconElement.ontouchend = anchorIconElement.onclick = event => { event.preventDefault(); noteElement.classList.toggle(NOTE_ANCHORED_CLASS); if (!noteElement.classList.contains(NOTE_ANCHORED_CLASS)) { deleteNoteRef(containerElement, noteId); addNoteRef(document.documentElement, noteId); } }; removeNoteElement.ontouchend = removeNoteElement.onclick = event => { event.preventDefault(); deleteNoteRef(containerElement, noteId); containerElement.remove(); }; noteElement.onmousedown = () => { selectNote(noteElement); }; function moveNote(event, deltaX, deltaY) { event.preventDefault(); const { clientX, clientY } = getPosition(event); noteElement.classList.add(NOTE_MOVING_CLASS); if (editable) { if (noteElement.classList.contains(NOTE_ANCHORED_CLASS)) { deleteNoteRef(containerElement, noteId); anchorElement = getTarget(clientX, clientY) || document.documentElement; addNoteRef(anchorElement, noteId); } else { anchorElement = document.documentElement; } } document.documentElement.insertBefore(containerElement, maskPageElement.getRootNode().host); noteElement.style.setProperty("left", (clientX - deltaX) + "px"); noteElement.style.setProperty("top", (clientY - deltaY) + "px"); noteElement.style.setProperty("position", "fixed"); displayMaskNote(); } function displayMaskNote() { if (anchorElement == document.documentElement || anchorElement == document.documentElement) { maskNoteElement.classList.remove(NOTE_MASK_MOVING_CLASS); } else { const boundingRectAnchor = anchorElement.getBoundingClientRect(); maskNoteElement.classList.add(NOTE_MASK_MOVING_CLASS); maskNoteElement.style.setProperty("top", boundingRectAnchor.y + "px"); maskNoteElement.style.setProperty("left", boundingRectAnchor.x + "px"); maskNoteElement.style.setProperty("width", boundingRectAnchor.width + "px"); maskNoteElement.style.setProperty("height", boundingRectAnchor.height + "px"); } } function selectNote(noteElement) { if (selectedNote) { selectedNote.classList.remove(NOTE_SELECTED_CLASS); } noteElement.classList.add(NOTE_SELECTED_CLASS); selectedNote = noteElement; } function getTarget(clientX, clientY) { const targets = Array.from(document.elementsFromPoint(clientX, clientY)).filter(element => element.tagName.toLowerCase() != NOTE_TAGNAME && !element.classList.contains(MASK_CLASS)); if (!targets.includes(document.documentElement)) { targets.push(document.documentElement); } let newTarget, target = targets[0], boundingRect = target.getBoundingClientRect(); newTarget = determineTargetElement("floor", target, clientX - boundingRect.left, getMatchedParents(target, "left")); if (newTarget == target) { newTarget = determineTargetElement("ceil", target, boundingRect.left + boundingRect.width - clientX, getMatchedParents(target, "right")); } if (newTarget == target) { newTarget = determineTargetElement("floor", target, clientY - boundingRect.top, getMatchedParents(target, "top")); } if (newTarget == target) { newTarget = determineTargetElement("ceil", target, boundingRect.top + boundingRect.height - clientY, getMatchedParents(target, "bottom")); } target = newTarget; while (boundingRect = target && target.getBoundingClientRect(), boundingRect && boundingRect.width <= SELECT_PX_THRESHOLD && boundingRect.height <= SELECT_PX_THRESHOLD) { target = target.parentElement; } return target; } function getMatchedParents(target, property) { let element = target, matchedParent, parents = []; do { const boundingRect = element.getBoundingClientRect(); if (element.parentElement && !element.parentElement.tagName.toLowerCase() != NOTE_TAGNAME && !element.classList.contains(MASK_CLASS)) { const parentBoundingRect = element.parentElement.getBoundingClientRect(); matchedParent = Math.abs(parentBoundingRect[property] - boundingRect[property]) <= SELECT_PX_THRESHOLD; if (matchedParent) { if (element.parentElement.clientWidth > SELECT_PX_THRESHOLD && element.parentElement.clientHeight > SELECT_PX_THRESHOLD && ((element.parentElement.clientWidth - element.clientWidth > SELECT_PX_THRESHOLD) || (element.parentElement.clientHeight - element.clientHeight > SELECT_PX_THRESHOLD))) { parents.push(element.parentElement); } element = element.parentElement; } } else { matchedParent = false; } } while (matchedParent && element); return parents; } function determineTargetElement(roundingMethod, target, widthDistance, parents) { if (Math[roundingMethod](widthDistance / SELECT_PX_THRESHOLD) <= parents.length) { target = parents[parents.length - Math[roundingMethod](widthDistance / SELECT_PX_THRESHOLD) - 1]; } return target; } }; const anchorNote = function anchorNote(event, noteElement, deltaX, deltaY) { event.preventDefault(); const { clientX, clientY } = getPosition(event); document.documentElement.style.removeProperty("user-select"); noteElement.classList.remove(NOTE_MOVING_CLASS); maskNoteElement.classList.remove(NOTE_MASK_MOVING_CLASS); maskPageElement.classList.remove(PAGE_MASK_ACTIVE_CLASS); const headerElement = noteElement.querySelector("header"); headerElement.ontouchmove = document.documentElement.onmousemove = null; let currentElement = anchorElement; let positionedElement; while (currentElement.parentElement && !positionedElement) { if (!FORBIDDEN_TAG_NAMES.includes(currentElement.tagName.toLowerCase())) { const currentElementStyle = getComputedStyle(currentElement); if (currentElementStyle.position != "static") { positionedElement = currentElement; } } currentElement = currentElement.parentElement; } if (!positionedElement) { positionedElement = document.documentElement; } const containerElement = noteElement.getRootNode().host; if (positionedElement == document.documentElement) { const firstMaskElement = document.querySelector("." + MASK_CLASS); document.documentElement.insertBefore(containerElement, firstMaskElement); } else { positionedElement.appendChild(containerElement); } const boundingRectPositionedElement = positionedElement.getBoundingClientRect(); const stylePositionedElement = window.getComputedStyle(positionedElement); const borderX = parseInt(stylePositionedElement.getPropertyValue("border-left-width")); const borderY = parseInt(stylePositionedElement.getPropertyValue("border-top-width")); noteElement.style.setProperty("position", "absolute"); noteElement.style.setProperty("left", (clientX - boundingRectPositionedElement.x - deltaX - borderX) + "px"); noteElement.style.setProperty("top", (clientY - boundingRectPositionedElement.y - deltaY - borderY) + "px"); }; const getPosition = function getPosition(event) { if (event.touches && event.touches.length) { const touch = event.touches[0]; return touch; } else { return event; } }; const onMouseUp = function onMouseUp(event) { if (highlightSelectionMode) { highlightSelection(); } if (removeHighlightMode) { let element = event.target, done; while (element && !done) { if (element.classList.contains(HIGHLIGHT_CLASS)) { document.querySelectorAll("." + HIGHLIGHT_CLASS + "[data-singlefile-highlight-id=" + JSON.stringify(element.dataset.singlefileHighlightId) + "]").forEach(highlightedElement => { resetHighlightedElement(highlightedElement); }); done = true; } element = element.parentElement; } } if (resizingNoteMode) { resizingNoteMode = false; document.documentElement.style.removeProperty("user-select"); maskPageElement.classList.remove(PAGE_MASK_ACTIVE_CLASS); document.documentElement.ontouchmove = document.documentElement.onmousemove = null; } if (movingNoteMode) { anchorNote(movingNoteMode.event || event, selectedNote, movingNoteMode.deltaX, movingNoteMode.deltaY); movingNoteMode = null; document.documentElement.ontouchmove = document.documentElement.onmousemove = null; } if (collapseNoteTimeout) { clearTimeout(collapseNoteTimeout); collapseNoteTimeout = null; } if (cuttingMode) { let element = event.target; if (document.documentElement != element && element.tagName.toLowerCase() != NOTE_TAGNAME) { element.classList.add(REMOVED_CONTENT_CLASS); removedElements.push(element); } } }; const maskNoteElement = getMaskElement("note-mask"); const maskPageElement = getMaskElement("page-mask", "single-file-page-mask"); let selectedNote, highlightSelectionMode, removeHighlightMode, resizingNoteMode, movingNoteMode, collapseNoteTimeout, cuttingMode; window.onresize = reflowNotes; document.documentElement.onmouseup = document.documentElement.ontouchend = onMouseUp; window.addEventListener("DOMContentLoaded", () => { processNode(document); reflowNotes(); document.querySelectorAll("single-file-note").forEach(noteElement => attachNoteListeners(noteElement)); }); })()</script><style>.single-file-highlight-yellow { background-color: #ffff7c !important; color: black !important; } span.single-file-highlight-yellow { display: inline !important; } .single-file-highlight-pink { background-color: #ffbbb6 !important; color: black !important; } span.single-file-highlight-pink { display: inline !important; } .single-file-highlight-blue { background-color: #95d0ff !important; color: black !important; } span.single-file-highlight-blue { display: inline !important; } .single-file-highlight-green { background-color: #93ef8d !important; color: black !important; } span.single-file-highlight-green { display: inline !important; } .single-file-highlight-hidden { background-color: inherit !important; color: inherit !important; } .single-file-mask { all: initial; display: contents !important; } .single-file-mask.single-file-page-mask { opacity: .99 !important; } single-file-note { all: initial !important; display: contents !important; } .single-file-hover { outline: 1px solid red !important; } .single-file-removed { display: none !important; float: none !important; position: static !important; visibility: collapse !important; }</style>